#pragma once

#include <GL/glew.h>

#include <SDL.h>

#include <png.h>

#include <math.h>

#define GLM_FORCE_RADIANS
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/quaternion.hpp>

using namespace glm;

#include <stack>
#include <map>
#include <string>
#include <set>
#include <vector>

#include "engine/Window.hpp"
#include "engine/Renderbuffer.hpp"
#include "engine/ShaderProgram.hpp"
#include "engine/FinalQuad.hpp"
#include "fog/Fog.hpp"
#include "light/Light.hpp"

class Scene* scene;

class Engine {
public:

	Window window;
	
	Renderbuffer renderbuffer;
	
private:

	unsigned int lightBufferCount = 0;
	
	GLuint lightBuffer = 0;

	static const char* vertexShaderSource;

	static const char* fragmentShaderSource;

	static const char* vertexFinalShaderSource;

	static const char* fragmentFinalShaderSource;
	
public:

	class SceneShader : public ShaderProgram {
		
		friend class Engine;
		
	private:
	
		struct Location {
		
			GLint projectionMatrix = -1;
			
			GLint modelViewMatrix = -1;
			
			struct Material {
			
				int enableTexture = -1; 
				
				int enableNormalTexture = -1;
				
				int diffuse = -1;
				
				int shininess = -1;
				
				int emission = -1;
				
				int textureOffset = -1;
				
			} material;
						
		} location;
		
	public:
	
		SceneShader(const char* vertexShaderSource, const char* fragmentShaderSource) : ShaderProgram(vertexShaderSource,fragmentShaderSource){
			glUseProgram(program);
			location.projectionMatrix = glGetUniformLocation(program,"projectionMatrix");
			location.modelViewMatrix = glGetUniformLocation(program,"modelViewMatrix");
			location.material.enableTexture = glGetUniformLocation(program,"material.enableTexture");
			location.material.enableNormalTexture = glGetUniformLocation(program,"material.enableNormalTexture");
			location.material.diffuse = glGetUniformLocation(program,"material.diffuse");
			location.material.shininess = glGetUniformLocation(program,"material.shininess");
			location.material.emission = glGetUniformLocation(program,"material.emission");
			location.material.textureOffset = glGetUniformLocation(program,"material.textureOffset");
			glUniform1i(glGetUniformLocation(program,"material.texture"),0);
			glUniform1i(glGetUniformLocation(program,"material.normalTexture"),1);
			setModelViewMatrix(mat4(1));
			glUseProgram(0);
		}
		
		void setTextureOffset(const glm::vec2& textureOffset){
			glUniform2fv(location.material.textureOffset,1,glm::value_ptr(textureOffset));
		}
		
		void setMaterialShininess(const float value){
			glUniform1f(location.material.shininess,value);
		}
		
		void setMaterialEmission(const float value){
			glUniform1f(location.material.emission,value);
		}

		void enableMaterialNormalTexture(){
			glUniform1ui(location.material.enableNormalTexture,GL_TRUE);
		} 
		
		void disableMaterialNormalTexture(){
			glUniform1ui(location.material.enableNormalTexture,GL_FALSE);
		}
		
		void enableMaterialTexture(){
			glUniform1ui(location.material.enableTexture,GL_TRUE);
		} 
		
		void disableMaterialTexture(){
			glUniform1ui(location.material.enableTexture,GL_FALSE);
		}
		
		void setMaterialDiffuse(const glm::vec4& diffuse){
			glUniform4fv(location.material.diffuse,1,glm::value_ptr(diffuse));
		}
		
		void setProjectionMatrix(const glm::mat4& matrix){
			glUniformMatrix4fv(location.projectionMatrix,1,GL_FALSE,value_ptr(matrix));
		}
		
		void setModelViewMatrix(const mat4& matrix){
			glUniformMatrix4fv(location.modelViewMatrix,1,GL_FALSE,value_ptr(matrix));
		}
		
	} shader;
	
	class FinalShader : public ShaderProgram {
		
		friend class Engine;
		
	private:
	
		struct Location {
		
			struct Fog {

				int enable = -1;
			
				int color = -1;
			
				int near = -1;
			
				int far = -1;
			
				int density = -1;
			
				int equation = -1;
				
			} fog;
			
		} location;
		
	public:
	
		void setFog(const Fog& fog){
			glUniform1ui(location.fog.enable, fog.enableFlag);
			glUniform1f(location.fog.density,fog.density);
			glUniform1f(location.fog.near,fog.near);
			glUniform1f(location.fog.far,fog.far);
			glUniform1f(location.fog.equation,fog.equation);
			glUniform3fv(location.fog.color,1,glm::value_ptr(fog.color));
		}
		
		FinalShader(const char* vertexShaderSource, const char* fragmentShaderSource) : ShaderProgram(vertexShaderSource,fragmentShaderSource){
		
			location.fog.enable = glGetUniformLocation(program,"fog.enable");
			location.fog.color = glGetUniformLocation(program,"fog.color");
			location.fog.near = glGetUniformLocation(program,"fog.near");
			location.fog.far = glGetUniformLocation(program,"fog.far");
			location.fog.density = glGetUniformLocation(program,"fog.density");
			location.fog.equation = glGetUniformLocation(program,"fog.equation");
			
			lightCountLocation = glGetUniformLocation(program,"lightCount"); 
			eyePositionLocation = glGetUniformLocation(program,"eyePosition"); 
			glUseProgram(program);
			glUniform1ui(lightCountLocation,0);
			glUseProgram(0);			
		}
	
	} finalShader;
	
	FinalQuad finalQuad;
	
private:

	Scene* activeScene = 0;	
	
public:
	
	void play(Scene* scene);
	
	void quit();
	
public:

	Engine();
	
	~Engine();
	
public:

	static int lightCountLocation;
	static int eyePositionLocation;
	
} engine;

int Engine::lightCountLocation = -1;
int Engine::eyePositionLocation = -1;

#include "scene/Scene.hpp"

Engine::Engine():
	shader(vertexShaderSource,fragmentShaderSource), 
	finalShader(vertexFinalShaderSource,fragmentFinalShaderSource)
{
	glGenBuffers(1,&lightBuffer);
}

Engine::~Engine(){
	glDeleteBuffers(1,&lightBuffer);
}

void Engine::play(Scene* scene){
	if (scene) if (!scene->playFlag){
		// Установка сцены
		if (activeScene) {
			activeScene->pauseFlag = true;
			activeScene->onPause();
		}
		Scene* previousScene = activeScene;
		activeScene = scene;
		scene->playFlag = true;
		scene->onPlay();
		unsigned int fpsTimer = SDL_GetTicks()+1000;
		unsigned int fpsCounter = 0;
		// Начинаем программный цикл
		while (scene->playFlag) {
			// Обработка событий
			SDL_Event event;
			while (SDL_PollEvent(&event)){
				switch (event.type) {
				case SDL_QUIT:
					quit();
					break;
				}
			}
			// Вызов кадрового события
			scene->onFrame();
			// Рендер
			if (scene->drawFlag && !scene->pauseFlag) {
				//renderer->renderFrame(scene);
				scene->drawFlag = false;

				// Рендерим модели
				glDisable(GL_BLEND); // настраиваем прозрачность
				GLint sFramebuffer; // запоминаем текуший фреймбуфер
				glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING,&sFramebuffer);
				shader.use(); // установка шейдера для мультитаргет рендера
				glBindFramebuffer(GL_DRAW_FRAMEBUFFER,renderbuffer.framebuffer);
				glClearColor(0,0,0,0);
				glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
				glDrawBuffers(3,Renderbuffer::drawBuffersData);
				glEnable(GL_CULL_FACE); // рендер только лицевых граней
				glEnable(GL_DEPTH_TEST); // включаем тест глубины
				glDepthFunc(GL_LESS);
				shader.setProjectionMatrix(scene->camera->getMatrix());
				scene->draw(); // рисуем сцену

				// Выводим гбуфер на экран
				glBindFramebuffer(GL_DRAW_FRAMEBUFFER,sFramebuffer); // востанавливаем фреймбуфер
				glDrawBuffer(GL_BACK); // ренедер только в один таргет
				scene->skyBox->draw(); // рисуем скайбокс, очистка экрана
				finalShader.use(); // шейдер финального рендера
				glActiveTexture(GL_TEXTURE2);
				glBindTexture(GL_TEXTURE_2D,renderbuffer.positionTexture);
				glActiveTexture(GL_TEXTURE1);
				glBindTexture(GL_TEXTURE_2D,renderbuffer.normalTexture);
				glActiveTexture(GL_TEXTURE0);
				glBindTexture(GL_TEXTURE_2D,renderbuffer.diffuseTexture);
				glDisable(GL_DEPTH_TEST); // отрубает тест глубины так как ненадо его тут
				glEnable(GL_BLEND); // настраиваем прозрачность
				glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

				/* ОСВЕЩЕНИЕ  */
				// установка числа источников освещениеfinalMatrixBuffer
				GLuint lightCount = scene->lightSet.size();
				glUniform1ui(lightCountLocation,lightCount);
				// Положение камеры 
				glUniform3fv(eyePositionLocation,1,glm::value_ptr(scene->camera->getPosition())); 
				// запись источников освещения в буфер
				glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, lightBuffer);	
				if (lightCount > lightBufferCount) {
					lightBufferCount = lightCount;
					glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(Light)*lightCount, NULL, GL_DYNAMIC_DRAW);
				}
				
				GLuint offset = 0;
				for (auto it = scene->lightSet.begin(); it != scene->lightSet.end(); ++it){
					glBufferSubData(GL_SHADER_STORAGE_BUFFER,offset,sizeof(Light),(*it));
					offset+=sizeof(Light);
				}
				/* ОСВЕЩЕНИЕ  */
				
				/* ТУМАН */
				if (scene->fog.updateFlag) {
					finalShader.setFog(scene->fog);
					scene->fog.updateFlag = false;
				}
				/* ТУМАН */
				
				finalQuad.draw();
				
				// Вывод окна на экрна
				window.swap();
				
				// Подсчет FPS
				fpsCounter++;
				if (SDL_GetTicks()>fpsTimer) {
					scene->onFps(fpsCounter);
					fpsCounter = 0;
					fpsTimer = SDL_GetTicks()+1000;
				}
			}
		}
		// Остановка сцены
		scene->onStop();
		activeScene = previousScene;
		if (activeScene) {
			activeScene->pauseFlag = false;
			activeScene->onResume();
		}
	}
}

void Engine::quit(){
	exit(0);
}

const char* Engine::vertexShaderSource = "#version 430 core\n\
layout (location = 0) in vec3 position;\
layout (location = 1) in vec3 normal;\
layout (location = 2) in vec2 texcoord;\
layout (location = 3) in int boneId;\
uniform mat4 projectionMatrix;\
uniform mat4 modelViewMatrix;\
out vec2 fragTexcoord;\
out vec3 fragNormal;\
out vec3 fragPosition;\
uniform struct {\
	bool enableTexture;\
	bool enableNormalTexture;\
	sampler2D texture;\
	sampler2D normalTexture;\
	vec4 diffuse;\
	float emission;\
	float shininess;\
	vec2 textureOffset;\
} material;\
uniform int boneCount;\
layout(std140, binding=0) buffer Bones {\
	mat4 bone[];\
};\
void main(){\
	vec4 worldPosition = modelViewMatrix * vec4(position,1);\
	fragNormal = mat3(modelViewMatrix) * normal;\
	if ((boneId>-1) && (boneId<boneCount)){\
		worldPosition = bone[boneId] * worldPosition;\
		fragNormal = mat3(bone[boneId]) * fragNormal;\
	}\
	fragPosition = worldPosition.xyz;\
	fragTexcoord = texcoord + material.textureOffset;\
	gl_Position = projectionMatrix * worldPosition;\
}";

const char* Engine::fragmentShaderSource = "#version 430 core\n\
in vec2 fragTexcoord;\
in vec3 fragNormal;\
in vec3 fragPosition;\
out vec4 out0;\
out vec4 out1;\
out vec4 out2;\
uniform struct {\
	bool enableTexture;\
	bool enableNormalTexture;\
	sampler2D texture;\
	sampler2D normalTexture;\
	vec4 diffuse;\
	float emission;\
	float shininess;\
	vec2 textureOffset;\
} material;\
void main(){\
	out0 = material.diffuse;\
	if (material.enableTexture)\
		 out0 *= texture(material.texture,fragTexcoord);\
	if (material.enableNormalTexture) {\
		vec3 normalMap = normalize(texture(material.normalTexture,fragTexcoord).xyz*2-1);\
		out1 = vec4(fragNormal+normalMap,material.shininess);\
	} else\
		out1 = vec4(fragNormal,material.shininess);\
	out2 = vec4(fragPosition,material.emission);\
}";

const char* Engine::vertexFinalShaderSource = "#version 430 core\n\
layout (location = 0) in vec4 vertexData;\
out vec2 fragTexcoord;\
void main(){\
	fragTexcoord = vertexData.zw;\
	gl_Position = vec4(vertexData.xy,0,1);\
}";

const char* Engine::fragmentFinalShaderSource = "#version 430 core\n\
layout (binding = 0) uniform sampler2D diffuseTexture;\
layout (binding = 1) uniform sampler2D normalTexture;\
layout (binding = 2) uniform sampler2D positionTexture;\
uniform vec3 eyePosition;\
uniform uint lightCount;\
struct Light{\
	vec4 param1; /*0 = enbl, 1 = constAtt, 2 = linAtt, 3 = quadAtt*/\
	vec4 param2; /*0..2 = pos, 3 = spec*/\
	vec4 param3; /*0..2 = diff, 3 = radius*/\
	vec4 param4; /*0 - inner radius, 1 - 1.0f / (radius - innerRadius);*/\
};\
uniform struct Fog{\
	bool enable;\
	vec3 color;\
	float far;\
	float near;\
	float density;\
	int equation;\
} fog;\
layout(std140, binding=1) buffer Lights {\
	Light light[];\
};\
float getFogFactor(Fog fog, float distance){\
   float result = 0.0;\
   if(fog.equation == 0)\
      return 1.0 - clamp((fog.far-distance)/(fog.far-fog.near),0,1);\
   else if(fog.equation == 1)\
      return 1.0 - clamp(exp(-fog.density*distance),0,1);\
   else if(fog.equation == 2)\
      return 1.0 - clamp(exp(-pow(fog.density*distance, 2.0)),0,1);\
   return 0;\
}\
in vec2 fragTexcoord;\
out vec4 finalColor;\
void main(){\
	vec4 fragmentPosition = texture(positionTexture,fragTexcoord);\
	float fragDistance = length(eyePosition - fragmentPosition.xyz);\
	float fogFactor = fog.enable ? getFogFactor(fog,fragDistance) : 0;\
	if (fogFactor < 1) { /* не считаем освещение если мы по уши в тумане*/\
		finalColor = texture(diffuseTexture,fragTexcoord);\
		if (lightCount > 0) {\
			/* освещение */\
			float attenuation;\
			vec4 fragmentNormal = texture(normalTexture,fragTexcoord);\
			fragmentNormal.xyz = normalize(fragmentNormal.xyz);\
			float fragmentShininess = fragmentNormal.w;\
			float fragmentEmission = fragmentPosition.w;\
			vec3 finalLightColor = vec3(0,0,0);\
			vec3 reflectedVector = reflect(-normalize(eyePosition - fragmentPosition.xyz),fragmentNormal.xyz);\
			for(uint i = 0; i < lightCount; ++i){\
				if (light[i].param1.x!=0) {\
					vec3 lightDir = light[i].param2.xyz - fragmentPosition.xyz;\
					float lightDistance = length(lightDir);\
					if (light[i].param3[3] >= 0){\
						if (lightDistance > light[i].param3[3])\
							continue;\
						attenuation = 1.0f - clamp(light[i].param4[1] * (lightDistance - light[i].param4[0]),0,1);\
					} else\
						attenuation = 1;\
					attenuation *= 1.0 / (light[i].param1.g + (light[i].param1.b*lightDistance)+(light[i].param1.a*lightDistance*lightDistance));\
					lightDir = normalize(lightDir);\
					/* Если вдруг решу что нестоит юзать diffuse>0,\
					   незабыть что diffuse надо max(dot(...),0)*/\
					float diffuseIntensity = dot(fragmentNormal.xyz,lightDir);\
					if (diffuseIntensity>0) {\
						/* интенсивность блика */\
						float specularIntensity = pow(max(dot(lightDir,reflectedVector),0),fragmentShininess);\
						finalLightColor += ((light[i].param3.rgb * diffuseIntensity) + (light[i].param2[3]*specularIntensity)) * attenuation;\
					}\
				}\
			}\
			finalColor.rgb *= finalLightColor;\
		}\
		finalColor.rgb = mix(finalColor.rgb, fog.color, fogFactor);\
	} else\
		finalColor.rgb = fog.color;\
}";


